﻿using System;
using System.Text;
using System.Text.RegularExpressions;

namespace FreshMeat.Storage
{
   /// <summary>
   /// Emulates the behavior of Windows wildcard file searches using '*' and '?'.
   /// </summary>
   /// <remarks>
   /// <para>
   /// This is intended for use on platforms that don't provide it (such as Windows Store Apps)
   /// or are broken (Windows Phone 7's implementation doesn't work with anything except '*').
   /// </para>
   /// <para>
   /// Some information on expected behavior is in the description for DirectoryInfo.GetFiles 
   /// (see http://msdn.microsoft.com/en-us/library/8he88b63.aspx).
   /// This does not attempt to emulate the described 8.3 file name behaviors.
   /// </para>
   /// </remarks>
   public class WildcardSearchEmulator
   {
      static readonly char[] pathTrimEndChars = new[] { '\t', '\n', '\v', '\f', '\r', ' ', '\x0085' };
      static readonly char[] pathInvalidChars = new[] { '"', '<', '>', '|', char.MinValue, '\x0001', '\x0002', '\x0003', '\x0004', '\x0005', '\x0006', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\x000E', '\x000F', '\x0010', '\x0011', '\x0012', '\x0013', '\x0014', '\x0015', '\x0016', '\x0017', '\x0018', '\x0019', '\x001A', '\x001B', '\x001C', '\x001D', '\x001E', '\x001F' };
      static readonly Regex extensionRegex = new Regex(@"^.*\.([^\.]+)$");

      readonly Regex regex;

      /// <summary>
      /// Creates instance for specified wildcard pattern.
      /// </summary>
      /// <param name="pattern">Wildcard pattern.</param>
      public WildcardSearchEmulator(string pattern)
      {
         regex = GenerateRegex(pattern);
      }

      /// <summary>
      /// Determines whethe the specified file name matches the pattern.
      /// </summary>
      /// <param name="name">File name including the extension (if it has one) but without any other path information.</param>
      /// <returns></returns>
      public bool IsMatch(string name)
      {
         return regex.IsMatch(name);
      }

      /// <summary>
      /// Generates a <see cref="Regex"/> from a Windows wildcard pattern which 
      /// closely emulates the behavior of DirectoryInfo.GetFiles.
      /// </summary>
      /// <param name="pattern">Pattern where '*' matches zero or more characters and '?' matches one character.</param>
      /// <returns>Regex generated from the pattern.</returns>
      public static Regex GenerateRegex(string pattern)
      {
         // This method is based on the pattern-to-regex function posted online here:
         // http://stackoverflow.com/questions/652037/how-do-i-check-if-a-filename-matches-a-wildcard-pattern?rq=1
         // The following changes have been made:
         // * Look for exact match when pattern does not contain wildcards (as GetFiles does)
         // * Don't trim pattern -- whitespace is significant (as GetFiles does)
         // * Empty pattern is allowed but doesn't match anything (as GetFiles does)

         if (pattern == null)
            throw new ArgumentNullException("pattern");

         // GetFiles trims certain characters from the end
         pattern = pattern.TrimEnd(pathTrimEndChars);

         // GetFiles treats the pattern "." as "*".
         if (pattern == ".")
            pattern = "*";

         if (pattern.IndexOfAny(pathInvalidChars) >= 0)
            throw new ArgumentException("Contains one or more invalid characters.", "pattern");

         var hasQuestion = pattern.IndexOf('?') >= 0;
         var hasAsterisk = pattern.IndexOf('*') >= 0;

         // From GetFiles documentation:
         //
         //    When using the asterisk wildcard character in a searchPattern (for example, "*.txt"), 
         //    the matching behavior varies depending on the length of the specified file extension. 
         //    A searchPattern with a file extension of exactly three characters returns files with an 
         //    extension of three or more characters, where the first three characters match the file 
         //    extension specified in the searchPattern. A searchPattern with a file extension of one,
         //    two, or more than three characters returns only files with extensions of exactly that 
         //    length that match the file extension specified in the searchPattern.
         //
         //    When using the question mark wildcard character, this method returns 
         //    only files that match the specified file extension.
         //
         // To simplify: if the pattern has an asterisk and a three character extension
         // which does not contain an asterisk then allow extra characters at the end.
         var allowExtraCharsAtEnd = false;
         if (hasAsterisk)
         {
            var extensionMatch = extensionRegex.Match(pattern);
            if (extensionMatch.Success)
            {
               var extension = extensionMatch.Groups[1];
               allowExtraCharsAtEnd = extension.Length == 3 && extension.Value.IndexOf('*') == -1;
            }
         }

         var builder = new StringBuilder();

         // Beginning of line
         builder.Append('^');

         // Escape all special characters including '*' and '?'
         builder.Append(Regex.Escape(pattern));

         // Turn '*' into expression meaning "zero or more characters"
         if (hasAsterisk)
            builder.Replace(@"\*", ".*");

            // Turn '?' into expression meaning "exactly one character"
         if (hasQuestion)
            builder.Replace(@"\?", ".");

         // If allowing extra characters at end then tack on expression that allows
         // any number of characters, except for dot to avoid matching ".abc.xyz".
         if (allowExtraCharsAtEnd)
            builder.Append(@"[^\.]*");

         // End of line
         builder.Append('$');

         var regex = new Regex(builder.ToString(), RegexOptions.IgnoreCase);
         return regex;
      }
   }
}